In [1]:
import random
def set_square(x, y, new_val, board):
"""
This function indexes into the given board at position (x, y).
We then change that value to new_val. Returns nothing.
"""
board[x][y] = new_val
def get_square(x, y, board):
"""
This function takes a board and returns the value at that square(x,y).
"""
return board[x][y]
def display_board(board):
print(*board, sep="\n")
def neighbour_squares(x, y, num_rows, num_cols):
"""
(x, y) 0-based index co-ordinate pair.
num_rows, num_cols: specifiy the max size of the board
returns all valid (x, y) coordinates from starting position.
"""
offsets = [(-1,-1), (-1,0), (-1,1),
( 0,-1), ( 0,1),
( 1,-1), ( 1,0), ( 1,1)]
result = []
for x2, y2 in offsets:
px = x + x2
py = y + y2
row_check = 0 <= px < num_rows
col_check = 0 <= py < num_cols
if row_check and col_check:
point = (py, px)
result.append(point)
return result
def count_occurence_of_character_in_neighbour_squares(x, y, board, character):
"""
returns the number of neighbours of (x,y) that are bombs. Max is 8, min is 0.
"""
num_rows = len(board[0])
num_cols = len(board)
squares = neighbour_squares(x, y, num_rows, num_cols)
character_found = 0
for px, py in squares:
square_value = get_square(px, py, board)
if square_value == character:
character_found += 1
return character_found
def build_board(num_rows, num_cols, bomb_count=0, non_bomb_character="-"):
board_temp = ["B"] * bomb_count + [non_bomb_character] * (num_rows * num_cols - bomb_count)
if bomb_count:
random.shuffle(board_temp)
board = []
for i in range(0, num_rows*num_cols, num_cols):
board.append(board_temp[i:i+num_cols])
return board
In [2]:
def flag_square(row, col, player_board):
p_square = get_square(row, col, player_board)
if p_square in "012345678":
# do nothing
return
## set flag
if p_square == EMPTY_SQUARE_CHARACTER:
set_square(row, col, FLAG_CHARACTER, player_board)
## Deflag if flag is already set
if p_square == FLAG_CHARACTER:
set_square(row, col, EMPTY_SQUARE_CHARACTER, player_board)
return
def reveal_square(row, col, player_board, game_board):
p_square = get_square(row, col, player_board)
if p_square in "012345678" or p_square == FLAG_CHARACTER:
## do nothing
return
g_square = get_square(row, col, game_board)
if g_square == BOMB_CHARACTER:
return game_over()
else:
bomb_count = count_occurence_of_character_in_neighbour_squares(row, col, game_board, BOMB_CHARACTER)
set_square(row, col, str(bomb_count), player_board)
def game_over():
print("GAME OVER")
## Later on, we can implement more logic here, such as asking if the player wants to play again.
Okay, so now that we have the reveal and flag functions sorted, we are dangeriously close to actually playing a game. Lets try and play...
In [3]:
import random
NUMBER_OF_ROWS = 3
NUMBER_OF_COLS = 3
NUMBER_OF_BOMBS = 2 # You may remember that all_caps mean that these variables should NOT change values at runtime.
BOMB_CHARACTER = "B"
FLAG_CHARACTER = "F"
EMPTY_SQUARE_CHARACTER = "-"
random.seed(213) # for reproducible results
game_board = build_board(NUMBER_OF_ROWS, NUMBER_OF_COLS, bomb_count = NUMBER_OF_BOMBS)
player_board = build_board(NUMBER_OF_ROWS, NUMBER_OF_COLS, bomb_count=0)
The above code should be familiar to you. The only addition I've made here is that I've added some character definitions. I thought it was good idea to define what the bomb_character is just once and have all the other functions reference that constant value.
When you start to glue the various peices of code together its perfectly normal to come up with new (and often better) ideas. So this means I'll need to go back and 'refactor' some of my older code to utilise this.
Anyway, how about we play a game?
In [4]:
print("INTERNAL GAME STATE:")
display_board(game_board)
print("")
print("PLAYER BOARD:")
display_board(player_board)
In [5]:
## Player board should display 2.
reveal_square(1, 0, player_board, game_board)
print("INTERNAL GAME STATE:")
display_board(game_board)
print("")
print("PLAYER BOARD:")
display_board(player_board)
Notice that the player_board reveals a 1 here. But there is clearly two bombs nearby. What this means is that I have a bug somewhere in my code.
It could be that my game_boards and player_boards are out of sync, or (more likely) I have some sort of indexing error in the "count_occurence_of_character_in_neighbour_squares" function. My best guess is that I've mixed up (x,y) coordinates somewhere.
But, in a way, this is a good thing. It's a good thing because it gives me an excuse to talk a bit more about testing and debugging code. In development there is a concept called "failing fast". At heart it's a simple idea; you should test your code quickly and often. And you should do that because it's much better to see code fail when it is small and simple than when its large and complex. If you have two thousand lines of code then that means there are two thousand possible places where the bug could be. If there are only twenty lines of code then there are only twenty possible places where the bug could be.
The other advantage of testing code as soon as possible is that the longer bugs go unnoticed the harder they are to fix.
Okay I've made a mental note of the error, but let's carry on for the moment. Maybe we shall find another bug...
In [7]:
## Square already revealed, nothing happens...
reveal_square(1, 0, player_board, game_board)
print("INTERNAL GAME STATE:")
display_board(game_board)
print("")
print("PLAYER BOARD:")
display_board(player_board)
In [8]:
## Flag a square
flag_square(2, 0, player_board)
print("INTERNAL GAME STATE:")
display_board(game_board)
print("")
print("PLAYER BOARD:")
display_board(player_board)
In [9]:
## Deflag square
flag_square(2, 0, player_board)
print("INTERNAL GAME STATE:")
display_board(game_board)
print("")
print("PLAYER BOARD:")
display_board(player_board)
In [10]:
## Reveal a bomb, Should display game over
reveal_square(1, 1, player_board, game_board)
print("INTERNAL GAME STATE:")
display_board(game_board)
print("")
print("PLAYER BOARD:")
display_board(player_board)
Okay so after a bit more testing it looks like we have something that is working fairly well. So the only things left to do now if to solve that bug...
In [ ]:
if row_check and col_check:
point = (py, px)
result.append(point)
return result
for px, py in squares: # result
square_value = get_square(px, py, board)
The above two code snippets belong to two different functions. Notice that there is some confusion here regarding px and py. In the first bit of code we have the values (px, py) but store them in (py, px) order. The the for-loop says for px, py do some stuff. This means the names get switched, py becomes px and vice versa. This is bad. Its bad because this sort of error (even if it is not the source of the bug) just leads to confusion, and confusion leads to bugs and wasted time. So the first thing to do would be to at least make the naming consistent across functions...
In [ ]:
if row_check and col_check:
point = (py, px)
result.append(point)
return result
for py, px in squares: # result
square_value = get_square(px, py, board)